/*
 * ClassFinder.java
 */
package eulerframework;

import java.io.*;
import java.net.URL;
import java.net.JarURLConnection;
import java.net.MalformedURLException;
import java.net.URLConnection;
import java.util.*;
import java.util.jar.*;

/**
 * This utility class was based originally on <a
 * href="private.php?do=newpm&u=47838">Daniel Le Berre</a>'s
 * <code>RTSI</code> class. This class can be called in different modes, but the
 * principal use is to determine what subclasses/implementations of a given
 * class/interface exist in the current runtime environment.
 *
 * @author Daniel Le Berre, Elliott Wade
 */
public class ClassFinder {

    private Class<?> searchClass = null;
    private Map<URL, String> classpathLocations = new HashMap<URL, String>();
    private Map<Class<?>, URL> results = new HashMap<Class<?>, URL>();
    private List<Throwable> errors = new ArrayList<Throwable>();
    private boolean working = false;

    public ClassFinder() {
        refreshLocations();
    }

    /**
     * Rescan the classpath, cacheing all possible file locations.
     */
    public final void refreshLocations() {
        synchronized (classpathLocations) {
            classpathLocations = getClasspathLocations();
        }
    }

    public final List<Class> findClasssFromSimpleName(String simpleClassName) {
        List<Class> foundClasses = new ArrayList<Class>();
        
        for(String classPathLocation : classpathLocations.values()) {
            String testPath = classPathLocation + "." + simpleClassName;
            
            try {
                Class c = Class.forName(testPath);
                foundClasses.add(c);
            }catch(ClassNotFoundException e) {
                
            }
        }
        
        return foundClasses;
    }
    
    /**
     * @param fqcn Name of superclass/interface on which to search
     */
    public final List<Class<?>> findSubclasses(String fqcn) {
        synchronized (classpathLocations) {
            synchronized (results) {
                try {
                    working = true;
                    searchClass = null;
                    errors = new ArrayList<Throwable>();
                    results = new TreeMap<Class<?>, URL>(CLASS_COMPARATOR);

                    //
                    // filter malformed FQCN
                    //
                    if (fqcn.startsWith(".") || fqcn.endsWith(".")) {
                        return new ArrayList<Class<?>>();
                    }

                    //
                    // Determine search class from fqcn
                    //
                    try {
                        searchClass = Class.forName(fqcn);
                    } catch (ClassNotFoundException ex) {
                        // if class not found, let empty vector return...
                        errors.add(ex);
                        return new ArrayList<Class<?>>();
                    }

                    return findSubclasses(searchClass, classpathLocations);
                } finally {
                    working = false;
                }
            }
        }
    }

    public final List<Throwable> getErrors() {
        return new ArrayList<Throwable>(errors);
    }

    /**
     * The result of the last search is cached in this object, along with the
     * URL that corresponds to each class returned. This method may be called to
     * query the cache for the location at which the given class was found.
     * <code>null</code> will be returned if the given class was not found
     * during the last search, or if the result cache has been cleared.
     */
    public final URL getLocationOf(Class<?> cls) {
        if (results != null) {
            return results.get(cls);
        } else {
            return null;
        }
    }

    /**
     * Determine every URL location defined by the current classpath, and it's
     * associated package name.
     */
    public final Map<URL, String> getClasspathLocations() {
        Map<URL, String> map = new TreeMap<URL, String>(URL_COMPARATOR);
        File file = null;

        String pathSep = System.getProperty("path.separator");
        String classpath = System.getProperty("java.class.path");

//        URL test = ClassFinder.class.getClassLoader().getResource("eventbus/findme.ini");
//        String webPath = test.toString();
//
//        if (webPath.contains("http")) {
//            webPath = webPath.substring(4, webPath.indexOf("!"));
//            System.out.println("Webpath: " + webPath);
//
//            includeJarHttp(webPath, map);
//        }


        StringTokenizer st = new StringTokenizer(classpath, pathSep);
        while (st.hasMoreTokens()) {
            String path = st.nextToken();

            file = new File(path);
            include(null, file, map);
        }

        return map;
    }
    private final static FileFilter DIRECTORIES_ONLY = new FileFilter() {

        public boolean accept(File f) {
            if (f.exists() && f.isDirectory()) {
                return true;
            } else {
                return false;
            }
        }
    };
    private final static Comparator<URL> URL_COMPARATOR = new Comparator<URL>() {

        public int compare(URL u1, URL u2) {
            return String.valueOf(u1).compareTo(String.valueOf(u2));
        }
    };
    private final static Comparator<Class<?>> CLASS_COMPARATOR = new Comparator<Class<?>>() {

        public int compare(Class<?> c1, Class<?> c2) {
            return String.valueOf(c1).compareTo(String.valueOf(c2));
        }
    };

    private final void include(String name, File file, Map<URL, String> map) {
        if (!file.exists()) {
            return;
        }
        if (!file.isDirectory()) {
            // could be a JAR file
            includeJar(file, map);
            return;
        }

        if (name == null) {
            name = "";
        } else {
            name += ".";
        }

        // add subpackages
        File[] dirs = file.listFiles(DIRECTORIES_ONLY);
        for (int i = 0; i < dirs.length; i++) {
            try {
                // add the present package
                map.put(
                        new URL("file://" + dirs[i].getCanonicalPath()),
                        name + dirs[i].getName());
            } catch (IOException ioe) {
                return;
            }

            include(name + dirs[i].getName(), dirs[i], map);
        }
    }

    private void includeJar(File file, Map<URL, String> map) {
        if (file.isDirectory()) {
            return;
        }

        URL jarURL = null;
        JarFile jar = null;
        try {
            jarURL = new URL("file:/" + file.getCanonicalPath());
            jarURL = new URL("jar:" + jarURL.toExternalForm() + "!/");
            JarURLConnection conn = (JarURLConnection) jarURL.openConnection();
            jar = conn.getJarFile();
        } catch (Exception e) {
            try {
                jarURL = new URL("file://" + file.getCanonicalPath());
                jarURL = new URL("jar:" + jarURL.toExternalForm() + "!/");
                JarURLConnection conn = (JarURLConnection) jarURL.openConnection();
                jar = conn.getJarFile();
            } catch (Exception ex) {
                // not a JAR or disk I/O error
                // either way, just skip
                return;
            }
        }

        if (jar == null || jarURL == null) {
            return;
        }

        // include the jar's "default" package (i.e. jar's root)
        map.put(jarURL, "");

        Enumeration<JarEntry> e = jar.entries();
        while (e.hasMoreElements()) {
            JarEntry entry = e.nextElement();

            if (entry.isDirectory()) {
                if (entry.getName().toUpperCase().equals("META-INF/")) {
                    continue;
                }

                try {
                    map.put(
                            new URL(jarURL.toExternalForm() + entry.getName()),
                            packageNameFor(entry));
                } catch (MalformedURLException murl) {
                    // whacky entry?
                    continue;
                }
            }
        }
    }

    private void includeJarHttp(String urlPath, Map<URL, String> map) {
        URL jarURL = null;
        JarFile jar = null;
        try {
            System.out.println("Before: [" + "jar:" + urlPath + "!/]");
            jarURL = new URL("jar:" + urlPath + "!/");
            System.out.println("Url: " + jarURL);
            JarURLConnection conn = (JarURLConnection) jarURL.openConnection();
            jar = conn.getJarFile();
        } catch (Exception e) {
            e.printStackTrace();
            return;
        }

        if (jar == null || jarURL == null) {
            return;
        }

        // include the jar's "default" package (i.e. jar's root)
        map.put(jarURL, "");

        Enumeration<JarEntry> e = jar.entries();
        while (e.hasMoreElements()) {
            JarEntry entry = e.nextElement();

            if (entry.isDirectory()) {
                if (entry.getName().toUpperCase().equals("META-INF/")) {
                    continue;
                }

                try {
                    map.put(
                            new URL(jarURL.toExternalForm() + entry.getName()),
                            packageNameFor(entry));
                } catch (MalformedURLException murl) {
                    // whacky entry?
                    continue;
                }
            }
        }
    }

    private static String packageNameFor(JarEntry entry) {
        if (entry == null) {
            return "";
        }
        String s = entry.getName();
        if (s == null) {
            return "";
        }
        if (s.length() == 0) {
            return s;
        }
        if (s.startsWith("/")) {
            s = s.substring(1, s.length());
        }
        if (s.endsWith("/")) {
            s = s.substring(0, s.length() - 1);
        }
        return s.replace('/', '.');
    }

    private final void includeResourceLocations(String packageName, Map<URL, String> map) {
        try {
            Enumeration<URL> resourceLocations =
                    ClassFinder.class.getClassLoader().getResources(getPackagePath(packageName));

            while (resourceLocations.hasMoreElements()) {
                map.put(
                        resourceLocations.nextElement(),
                        packageName);
            }
        } catch (Exception e) {
            // well, we tried
            errors.add(e);
            return;
        }
    }

    private final List<Class<?>> findSubclasses(
            Class<?> superClass, Map<URL, String> locations) {
        List<Class<?>> v = new ArrayList<Class<?>>();

        List<Class<?>> w = null;

        Iterator<URL> it = locations.keySet().iterator();
        while (it.hasNext()) {
            URL url = it.next();

            w = findSubclasses(url, locations.get(url), superClass);
            if (w != null && (w.size() > 0)) {
                v.addAll(w);
            }
        }

        return v;
    }

    private List<Class<?>> findSubclasses(
            URL location, String packageName, Class<?> superClass) {

        synchronized (results) {

            // hash guarantees unique names...
            Map<Class<?>, URL> thisResult = new TreeMap<Class<?>, URL>(CLASS_COMPARATOR);
            List<Class<?>> v = new ArrayList<Class<?>>(); // ...but return a vector

            // TODO: double-check for null search class
            String fqcn = searchClass.getName();

            List<URL> knownLocations = new ArrayList<URL>();
            knownLocations.add(location);
            // TODO: add getResourceLocations() to this list

            // iterate matching package locations...
            for (int loc = 0; loc < knownLocations.size(); loc++) {
                URL url = knownLocations.get(loc);

                // Get a File object for the package
                File directory = new File(url.getFile());

                //System.out.println ("\tlooking in " + directory);

                if (directory.exists()) {
                    // Get the list of the files contained in the package
                    String[] files = directory.list();
                    for (int i = 0; i < files.length; i++) {
                        // we are only interested in .class files
                        if (files[i].endsWith(".class")) {
                            // removes the .class extension
                            String classname = files[i].substring(0, files[i].length() - 6);

                            try {
                                Class<?> c = Class.forName(packageName + "." + classname);
                                if (superClass.isAssignableFrom(c)
                                        && !fqcn.equals(packageName + "." + classname)) {
                                    thisResult.put(c, url);
                                }
                            } catch (ClassNotFoundException cnfex) {
                                errors.add(cnfex);
                            } catch (Exception ex) {
                                errors.add(ex);
                            }
                        }
                    }
                } else {
                    try {
                        // It does not work with the filesystem: we must
                        // be in the case of a package contained in a jar file.
                        URLConnection openConnection = url.openConnection();

                        if (openConnection instanceof JarURLConnection) {
                            JarURLConnection conn = (JarURLConnection) openConnection;
                            //String starts = conn.getEntryName();
                            JarFile jarFile = conn.getJarFile();

                            Enumeration<JarEntry> e = jarFile.entries();

                            while (e.hasMoreElements()) {
                                JarEntry entry = e.nextElement();

                                String entryname = entry.getName();

                                if (!entry.isDirectory() && entryname.endsWith(".class")) {

                                    String classname = entryname.substring(0, entryname.length() - 6);
                                    if (classname.startsWith("/")) {
                                        classname = classname.substring(1);
                                    }
                                    classname = classname.replace('/', '.');
                                    try {
                                        // TODO: verify this block
                                        Class c = Class.forName(classname, false, this.getClass().getClassLoader());
                                        if (superClass.isAssignableFrom(c)
                                                && !fqcn.equals(classname)) {
                                            thisResult.put(c, url);
                                        }
                                    } catch (ClassNotFoundException cnfex) {
                                        // that's strange since we're scanning
                                        // the same classpath the classloader's
                                        // using... oh, well
                                        errors.add(cnfex);
                                    } catch (NoClassDefFoundError ncdfe) {
                                        // dependency problem... class is
                                        // unusable anyway, so just ignore it
                                        errors.add(ncdfe);
                                    } catch (UnsatisfiedLinkError ule) {
                                        // another dependency problem... class is
                                        // unusable anyway, so just ignore it
                                        errors.add(ule);
                                    } catch (Exception exception) {
                                        // unexpected problem
                                        //System.err.println (ex);
                                        errors.add(exception);
                                    } catch (Error error) {
                                        // lots of things could go wrong
                                        // that we'll just ignore since
                                        // they're so rare...
                                        errors.add(error);
                                    }
                                }
                            }
                        }
                    } catch (IOException ioex) {
                        errors.add(ioex);
                    }
                }
            } // while

            results.putAll(thisResult);

            Iterator<Class<?>> it = thisResult.keySet().iterator();
            while (it.hasNext()) {
                v.add(it.next());
            }

            return v;

        } // synch results
    }

    private final static String getPackagePath(String packageName) {
        // Translate the package name into an "absolute" path
        String path = new String(packageName);
        if (!path.startsWith("/")) {
            path = "/" + path;
        }
        path = path.replace('.', '/');

        // ending with "/" indicates a directory to the classloader
        if (!path.endsWith("/")) {
            path += "/";
        }

        // for actual classloader interface (NOT Class.getResource() which
        //  hacks up the request string!) a resource beginning with a "/"
        //  will never be found!!! (unless it's at the root, maybe?)
        if (path.startsWith("/")) {
            path = path.substring(1, path.length());
        }

        return path;
    }
}
